package event;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import event.EQueueProxy;
import event.Event;
import node.NodeDescriptor;

/**
 * This class implements the interface EQueue and is a proxy which is representative of the server near the client   
 * Contains the methods:
 * * enqueue
 * * dequeue
 * * sendEventRequest
 * * getRemoteDequeueEvent
 * * sendAvailabilityRequestAndWaitResponse
 * 
 * @see EQueue
 * @author 
 * 
 */
public class EQueueProxy implements EQueue{
	
	public rEQueue realQueue;
	protected String destinationIP;
	protected int destinationPort;
	
	// Configurables
	protected int serverPort;
	protected int maxThreadsNumber;

	
	/**
	 * Default builder. It instances the instance of the real queue 
	 * @param host a string that representing the destination IP
	 * @param port a string that representing the destination port
	 * 
	 */
	public EQueueProxy(String host, String port)
	{
		super();
		realQueue 		= new rEQueue();
		destinationIP 	= host;
		destinationPort	= new Integer(port).intValue(); 
	}

	/**
	 * This method has used from the ServerQueueProxy to send a UDP packet which contains a event that will be forward at a remote node
	 * Two UDP packet have created: one packet has used to forward the message, the other has used to receive the response.
	 * Communication takes place using a DatagramSocket
	 * 
	 * @param e The event forwarded to remote node. Information about receiver has inserted command attribute of the event
	 * @see NodeDescriptor
	 * @exception UnknownHostException
	 * @exception SocketException
	 * @exception IOException
	 * @return An integer value representing  
	 */
	@Override
	public int enqueue(Event e) {
		
		InetAddress destinationAddress 	= null;
		
		try {
			destinationAddress 	= InetAddress.getByName(destinationIP);
							
			// SEND A COMMAND AVAILABILITY REQUEST
			System.out.println(" >> EventProxy - Enqueue: Sending Command Availability Request - " + NodeDescriptor.getInstance().getEES().getCodice());
			int availabilityResponse = this.sendAvailabilityRequestAndWaitResponse(destinationAddress, destinationPort, NodeDescriptor.getInstance().getEES().getCodice());

			if(availabilityResponse == 0)
			{
				// SEND THE EVENT REQUEST
				System.out.println(" >> EventProxy - Enqueue: Command Accepted from Remote Node. Sending Event: " + e);
				return this.sendEventRequest(destinationAddress, destinationPort, e);
			}
			else
				System.out.println(" >> EventProxy - Enqueue: Command Not Accepted from Remote Node. Stop.");
				
		} catch (UnknownHostException ex) {
			// ERRORE: 1 - Impossibile identificare il nodo destinatario
			// Error 1 - Impossible identify receiver node
			System.out.println(" >> EventProxy - Enqueue: Destination Host is Unknown." + ex.getMessage());
			return EQueueProxy.COMM_UNKNOWN_HOST_ERROR;
		} catch(SocketException ex) {
			// ERRORE: 2 - Impossibile creare la DatagramSocket
			// Error 2 - Impossible create DatagramSocket	
			System.out.println(" >> EventProxy - Enqueue: Cannot Create DatagramSocket." + ex.getMessage());
			return EQueueProxy.COMM_UDP_SOCKET_ERROR;
		}
		catch(IOException ex) {
			// ERRORE: 3 - Impossibile inviare/ricevere l'UDP Datagram
			// Error 3 - Impossible send/receive UDP Datagram
			System.out.println(" >> EventProxy - Enqueue: Cannot send/receive UPD Packet." + ex.getMessage());
			return EQueueProxy.COMM_UDP_IO_ERROR;
		}
		return EQueueProxy.COMM_OK;
	}

	
	/**
	 * This method has used from the ServerQueueProxy to send a UDP packet which contains a availability request and if the 
	 * request has accepted then get a event from remote node. 
	 * 
	 * @exception SocketException
	 * @exception IOException
	 * @exception ClassNotFoundException
	 * @return A event from remote node
	 */
	@Override
	public Event dequeue() {

		// TODO Auto-generated method stub
		InetAddress destinationAddress 	= null;

		try {
			destinationAddress 	= InetAddress.getByName(destinationIP);
							
			// SEND A COMMAND AVAILABILITY REQUEST
			System.out.println(" >> EventProxy - Dequeue: Sending Command Availability Request - " + NodeDescriptor.getInstance().getEES().getCodice());
			int availabilityResponse = this.sendAvailabilityRequestAndWaitResponse(destinationAddress, destinationPort, NodeDescriptor.getInstance().getEES().getCodice());

			if(availabilityResponse == 0)
			{
				// SEND THE EVENT REQUEST
				System.out.println(" >> EventProxy - Dequeue: Command Accepted from Remote Node. Waiting for dequeued Event.");
				return this.getRemoteDequeuedEvent();
			}
			else
				System.out.println(" >> EventProxy - Dequeue: Command Not Accepted from Remote Node. Stop.");
			
		} catch(SocketException e) {
			// ERRORE: 2 - Impossibile creare la DatagramSocket
			System.out.println(" >> EventProxy - Dequeue: Cannot Create DatagramSocket." + e.getMessage());
		} catch(IOException e) {
			// ERRORE: 3 - Impossibile inviare/ricevere l'UDP Datagram
			System.out.println(" >> EventProxy - Dequeue: Cannot send/receive UPD Packet." + e.getMessage());
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			System.out.println(" >> EventProxy - Dequeue: Cannot Cast to Event Class." + e.getMessage());
		}
		return null;	
	}
		
	
	/**
	 * Method used to implement the mechanism of communication.
	 * It is:
	 * 	* Created a DatagramPacket to send
	 *  * DatagramSocket has instantiated and initialized    
	 *  * Send UDP packet
	 *  * Socket sets up into reception mode
	 *  * Response packet has received (if there aren't error)
	 * @param destinationAddress IP address of receiver 
	 * @param port Reception port of receiver
	 * @param payload Information to send. It contains information about the event
	 * @return An integer positive value if the procedure is successfully completed and a valid packet has received
	 * 
	 * @see java.io.ByteArrayInputStream
	 * @see java.io.ByteArrayOutputStream
	 * @see java.net.DatagramPacket
	 * @see java.net.DatagramSocket
	 * 
	 * @throws UnknownHostException
	 * @throws IOException
	 * @throws SocketException
	 */
	private int sendEventRequest(InetAddress destinationAddress, int destinationPort, Event e) throws UnknownHostException, IOException, SocketException 
	{
		// Serialize to a byte array
	    ByteArrayOutputStream bos = new ByteArrayOutputStream();
	    ObjectOutputStream out = new ObjectOutputStream(bos) ;
	    out.writeObject(e);
	    out.flush();
	    out.close();

	    // Get the bytes of the serialized object
	    byte[] payload = bos.toByteArray();
		
	    DatagramPacket udpPacket = new DatagramPacket(payload, payload.length, destinationAddress, destinationPort);
		
		// DATAGRAM SOCKET - SEND
		DatagramSocket udpSocket = new DatagramSocket();
		udpSocket.send(udpPacket);
		udpSocket.close();

		return 0;
	}
	
	/**
	 * Method used to implement the mechanism of communication.
	 * It is:
	 * 	* Created a DatagramPacket to receive remote Event
	 *  * Receive UDP packet
	 *  
	 * @see java.io.ByteArrayInputStream
	 * @see java.io.ByteArrayOutputStream
	 * @see java.net.DatagramPacket
	 * @see java.net.DatagramSocket
	 *  
	 * @return a remote Event
	 * 
	 * @throws SocketException
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	private Event getRemoteDequeuedEvent() throws SocketException, IOException, ClassNotFoundException
	{
		// Initialize an UDP Packet to receive Remote Event
		byte[] payload = new byte[65536];
		DatagramPacket udpPacket = new DatagramPacket(payload, payload.length);
		
		// Receive the Datagram Packet
		DatagramSocket udpSocket = new DatagramSocket();
		udpSocket.receive(udpPacket);
		
		// Unmarshall the Event From ByteArrayInputStream
		ByteArrayInputStream bais = new ByteArrayInputStream(udpPacket.getData());
		ObjectInputStream ois = new ObjectInputStream(bais);
		Event receivedEvent = (Event) ois.readObject();
		ois.close();
		
		return receivedEvent;
	}
	
	/**
	 * This method send a datagram packet containing the function request, wait to receive the acknowledge and return the 
	 * corresponding integer value  
	 * @param destinationAddress
	 * @param destinationPort
	 * @param command
	 * 
	 * @see java.io.ByteArrayInputStream
	 * @see java.io.ByteArrayOutputStream
	 * @see java.net.DatagramPacket
	 * @see java.net.DatagramSocket
	 * @see node.NodeDescriptor
	 * 
	 * @return An integer value representing the received acknowledge
	 * 
	 * @throws SocketException
	 * @throws IOException
	 */
	
	private int sendAvailabilityRequestAndWaitResponse(InetAddress destinationAddress,int destinationPort,String command) throws SocketException, IOException
					
	{
		DatagramSocket udpSocket = null;
		DatagramPacket udpPacket = null;
		int readTimeout	 = NodeDescriptor.getInstance().getE_timeout();
		
		// Serialize to a byte array
		
	    ByteArrayOutputStream bos = new ByteArrayOutputStream();
	    ObjectOutputStream out = new ObjectOutputStream(bos) ;
	    out.writeUTF(command);
	    out.flush();
	    out.close();

	    // Get the bytes of the serialized object
	    byte[] payload = bos.toByteArray();
		
		udpPacket = new DatagramPacket(payload, payload.length, destinationAddress, destinationPort);
		
		// DATAGRAM SOCKET - SEND FUNCTION REQUEST
		udpSocket = new DatagramSocket();
		udpSocket.send(udpPacket);

		// DATAGRAM SOCKET - RECEIVE ACK
		byte[] buffer = new byte[1024];
		DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length);
		udpSocket.setSoTimeout(readTimeout);
		udpSocket.receive(responsePacket);
		udpSocket.close();
			
		// RETURN THE CORRESPONDING INT
		if(responsePacket!=null && responsePacket.getLength()>0)
		{
			// OK: UDP Datagram Received.
			System.out.println(this.destinationPort);
			this.destinationPort = responsePacket.getPort();
			System.out.println(this.destinationPort);
			
			payload = responsePacket.getData();
			ByteArrayInputStream bais = new ByteArrayInputStream(payload);
			ObjectInputStream ois = new ObjectInputStream(bais);
			int response = ois.readInt();
			ois.close();
			
			return response;
		}
		return EQueue.COMM_UNDEFINED_ERROR;
	}
}